package com.bc.plugin.doc.resolver;

import com.bc.plugin.doc.builder.BuilderConfig;
import com.bc.plugin.doc.builder.JavaClassFileBuilder;
import com.bc.plugin.doc.dto.FieldClassDTO;
import com.bc.plugin.doc.resolver.help.ApiParamHelper;
import com.bc.plugin.doc.resolver.help.ClassTypeUtil;
import com.bc.plugin.doc.resolver.help.JavaClassUtil;
import com.bc.plugin.doc.resolver.help.JavaFieldHelper;
import com.power.doc.model.ApiParam;
import com.power.doc.utils.DocClassUtil;
import com.thoughtworks.qdox.model.JavaClass;
import com.thoughtworks.qdox.model.JavaField;
import com.power.doc.constants.DocGlobalConstants;
import lombok.Data;
import lombok.ToString;

import java.util.*;

/**
 * @author xhh
 */
@Data
public class ApiParamResolver {
    private List<IApiParamHandle> apiParamHandles;
    private BuilderConfig builderConfig;
    private JavaClassFileBuilder javaClassFileBuilder;

    public ApiParamResolver(BuilderConfig builderConfig) {
        this.builderConfig = builderConfig;
        this.javaClassFileBuilder = builderConfig.getJavaClassFileBuilder();
        this.apiParamHandles = builderConfig.getApiParamHandles();
    }

    public List<ApiParam> resolverRequestParams(String className, String paramName, Map<String, String> paramsComments, boolean isRequired) {
        ClassTypeData classTypeData = new ClassTypeData(className);
        if (classTypeData.isSimpleType) {
            String processedType = DocClassUtil.processTypeNameForParams(classTypeData.outerRealName);
            ApiParam apiParam = ApiParam.of().setLevel(0).setDesc(paramsComments.get(paramName))
                    .setType(processedType).setField(paramName)
                    .setVersion(DocGlobalConstants.DEFAULT_VERSION).setRequired(isRequired);
            List<ApiParam> paramList = new ArrayList<>();
            addApiParam(paramList, apiParam);
            return paramList;
        }
        return resolverParams(classTypeData, false);
    }

    public List<ApiParam> resolverResponseParams(String className) {
        ClassTypeData classTypeData = new ClassTypeData(className);
        if (classTypeData.isSimpleType) {
            ApiParam apiParam = ApiParamHelper.newApiParam(classTypeData.outerRealName, 0);
            List<ApiParam> paramList = new ArrayList<>();
            addApiParam(paramList, apiParam);
            return paramList;
        }
        return resolverParams(classTypeData, true);
    }

    private List<ApiParam> resolverParams(ClassTypeData classTypeData, boolean isResponse) {
        List<ApiParam> paramList = new ArrayList<>();
        Stack<FieldClassDTO> stack = new Stack<>();
        stack.addAll(getFieldClassDTOList(classTypeData, 0));
        Map<String, String> registryClasses = new HashMap<>();
        registryClasses.put(classTypeData.unwrapJavaUtilTypeName, "$TOP");
        out : while (!stack.isEmpty()) {
            FieldClassDTO fieldClassDTO = stack.pop();
            JavaField javaField = fieldClassDTO.getJavaField();
            String fieldName = javaField.getName();
            if ("this$0".equals(fieldName) || "serialVersionUID".equals(fieldName) || "$jacocoData".equals(fieldName)) {
                continue;
            }
            Map<String, String> fieldTypeMap = JavaFieldHelper.getFieldTypeMap(fieldClassDTO.getGenericCanonicalName());
            Optional<String> genericTypeField = Optional.ofNullable(fieldTypeMap).map(map -> map.get(fieldName));
            String fieldClassName = genericTypeField.orElse(javaField.getType().getGenericCanonicalName());
            ClassTypeData fieldClassTypeData = new ClassTypeData(fieldClassName);
            ApiParam apiParam;
            if (genericTypeField.isPresent()) {
                apiParam = ApiParamHelper.newApiParam(javaField, fieldClassTypeData.outerRealName, fieldClassDTO.getLevel());
            } else {
                apiParam = ApiParamHelper.newApiParam(javaField, fieldClassDTO.getLevel());
            }
            for (IApiParamHandle apiParamHandle : apiParamHandles) {
                apiParam = apiParamHandle.handle(apiParam, fieldClassDTO, isResponse);
                if (apiParam == null) {
                    continue out;
                }
            }
            addApiParam(paramList, apiParam);
            //处理<>内部类型
            if (fieldClassTypeData.isSimpleType || javaField.getType().isEnum()) {
                continue;
            }
            // Check circular reference
            String refField = registryClasses.get(fieldClassTypeData.unwrapJavaUtilTypeName);
            if (refField != null) {
                apiParam = ApiParam.of().setField("$ref").setDesc("ref field body -> " + refField)
                        .setLevel(fieldClassDTO.getLevel() + 1).setType("ref").setVersion(apiParam.getVersion());
                addApiParam(paramList, apiParam);
                continue;
            }
            // Registry class
            registryClasses.put(fieldClassTypeData.unwrapJavaUtilTypeName, fieldName + ", level " + (fieldClassDTO.getLevel() + 1));
            stack.addAll(getFieldClassDTOList(fieldClassTypeData, fieldClassDTO.getLevel() + 1));
        }
        return paramList;
    }

    private List<FieldClassDTO> getFieldClassDTOList(ClassTypeData classTypeData, int level) {
        JavaClass javaClass = javaClassFileBuilder.getModelClassByName(classTypeData.outerClassName);
        List<JavaField> fields = JavaClassUtil.getFields(javaClass);
        List<FieldClassDTO> list = new ArrayList<>();
        for (JavaField javaField : fields) {
            list.add(0, new FieldClassDTO(classTypeData.unwrapJavaUtilTypeName, javaField, level));
        }
        return list;
    }

    private void addApiParam(List<ApiParam> paramList, ApiParam apiParam) {
        for (IApiParamHandle apiParamHandle : apiParamHandles) {
            apiParamHandle.handleApiParam(apiParam);
        }
        paramList.add(apiParam);
    }

    @ToString
    private static class ClassTypeData {
        String className;
        String outerRealName;
        String outerClassName;
        String unwrapJavaUtilTypeName;
        boolean isSimpleType;

        private ClassTypeData(String className) {
            this.className = className;//List<Resp<List<UserDto>>>
            this.outerRealName = DocClassUtil.getSimpleName(className);//List
            this.unwrapJavaUtilTypeName = ClassTypeUtil.unwrapJavaUtilType(className);//Resp<List<UserDto>>

            this.outerClassName = DocClassUtil.getSimpleName(unwrapJavaUtilTypeName);//Resp
            this.isSimpleType = ClassTypeUtil.isSimpleType(outerClassName);//false
        }
    }

}
